{-----------------------------------------------------------------------------
 Unit Name: dlgUnitTestWizard
 Author:    Kiriakos Vlahos
 Date:      09-Feb-2006
 Purpose:   Unit Test Wizard
 History:
-----------------------------------------------------------------------------}
unit dlgUnitTestWizard;

interface

uses
  System.UITypes,
  System.Classes,
  System.ImageList,
  Vcl.Controls,
  Vcl.Menus,
  Vcl.ExtCtrls,
  Vcl.StdCtrls,
  Vcl.ImgList,
  Vcl.VirtualImageList,
  TB2Item,
  SpTBXItem,
  VirtualTrees.Types,
  VirtualTrees.BaseAncestorVCL,
  VirtualTrees.AncestorVCL,
  VirtualTrees.BaseTree,
  VirtualTrees,
  dlgPyIDEBase,
  XLSPTypes;

type
  TUnitTestWizard = class(TPyIDEDlgBase)
    Panel1: TPanel;
    ExplorerTree: TVirtualStringTree;
    Bevel1: TBevel;
    PopupUnitTestWizard: TSpTBXPopupMenu;
    mnSelectAll: TSpTBXItem;
    mnDeselectAll: TSpTBXItem;
    Label1: TLabel;
    lbHeader: TLabel;
    lbFileName: TLabel;
    OKButton: TButton;
    BitBtn2: TButton;
    HelpButton: TButton;
    vilCodeImages: TVirtualImageList;
    vilImages: TVirtualImageList;
    procedure HelpButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure ExplorerTreeInitNode(Sender: TBaseVirtualTree; ParentNode,
      Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
    procedure ExplorerTreeInitChildren(Sender: TBaseVirtualTree;
      Node: PVirtualNode; var ChildCount: Cardinal);
    procedure ExplorerTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
    procedure ExplorerTreeGetImageIndex(Sender: TBaseVirtualTree;
      Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
      var Ghosted: Boolean; var ImageIndex: TImageIndex);
    procedure ExplorerTreeGetHint(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; var LineBreakStyle: TVTTooltipLineBreakStyle;
      var HintText: string);
    procedure mnSelectAllClick(Sender: TObject);
    procedure mnDeselectAllClick(Sender: TObject);
    function SymbolFromNode(Node: PVirtualNode): TLSPDocumentSymbol;
  private
    Symbols: TLSPDocumentSymbols;
    FModuleName: string;
    FModuleFileName: string;
    function SymbolHint(Symbol: TLSPDocumentSymbol): string;
    function SymbolSignature(Symbol: TLSPDocumentSymbol): string;
  public
    class function GenerateTests(const ModuleFName: string): string;
  end;

implementation

uses
  System.SysUtils,
  System.Generics.Collections,
  Vcl.Forms,
  JvJVCLUtils,
  JvGnugettext,
  dmResources,
  uCommonFunctions,
  SynEditTypes,
  cLSPClients;

{$R *.dfm}

procedure Prune(var Symbols: TLSPDocumentSymbols; ModuleFileName: string);

  function IsImported(Symbol: TLSPDocumentSymbol): Boolean;
  begin
      var Line := Symbol.selectionRange.start.line;
      var LineSource := GetNthSourceLine(ModuleFileName, Line + 1).TrimLeft;
      Result := LineSource.StartsWith('from') or
        ((Symbol.Kind = TLspSymbolKind.symFunction) and
         not LineSource.StartsWith('def'));
  end;

  function PruneChildren(var Symbol: TLSPDocumentSymbol; IsClass: Boolean): Boolean;
  begin
    for var I := High(Symbol.Children) downto Low(Symbol.Children) do
    begin
       if not IsClass or (Symbol.Children[I].kind <> TLSPSymbolKind.symMethod) then
         Delete(Symbol.Children, I, 1);
    end;
    Result := IsClass and (Length(Symbol.Children) = 0);
  end;

begin
  for var I := High(Symbols) downto Low(Symbols) do
  begin
     if not (Symbols[I].Kind in [TLSPSymbolKind.symClass, TLSPSymbolKind.symFunction]) or
       IsImported(Symbols[I]) or
       PruneChildren(Symbols[I], Symbols[I].Kind = TLSPSymbolKind.symClass)
     then
       Delete(Symbols, I, 1);
  end;
end;

class function TUnitTestWizard.GenerateTests(const ModuleFName: string): string;

const
  Header = '#This file was originally generated by PyScripter''s unit test wizard' +
    sLineBreak + sLineBreak + 'import unittest'+ sLineBreak + 'import %s'
    + sLineBreak + sLineBreak;

   ClassHeader =
      'class Test%s(unittest.TestCase):'+ sLineBreak + sLineBreak +
      '    def setUp(self): ' + sLineBreak +
      '        pass' + sLineBreak + sLineBreak +
      '    def tearDown(self): ' + sLineBreak +
      '        pass' + sLineBreak + sLineBreak;

    MethodHeader =
        '    def test_%s(self):' + sLineBreak +
        '        pass' + sLineBreak + sLineBreak;

     Footer =
      'if __name__ == ''__main__'':' + sLineBreak +
      '    unittest.main()' + sLineBreak;

var
  Node, MethodNode: PVirtualNode;
  Symbol, MSymbol: TLSPDocumentSymbol;
  SName, MName: string;
  FunctionTests: string;
  WaitCursorInterface: IInterface;
begin
  Result := '';
  FunctionTests := '';

  var LSymbols := TPyLspClient.DocumentSymbols(ModuleFName);
  if not Assigned(LSymbols) then Exit;
  Prune(LSymbols, ModuleFName);
  if Length(LSymbols) = 0 then Exit;

  with TUnitTestWizard.Create(Application) do
  begin
    lbFileName.Caption := ModuleFName;
    Symbols := LSymbols;
    FModuleFileName := ModuleFName;
    FModuleName := FileNameToModuleName(ModuleFName);
    // Turn off Animation to speed things up
    ExplorerTree.TreeOptions.AnimationOptions :=
      ExplorerTree.TreeOptions.AnimationOptions - [toAnimatedToggle];
    ExplorerTree.RootNodeCount := 1;
    ExplorerTree.TreeOptions.AnimationOptions :=
      ExplorerTree.TreeOptions.AnimationOptions + [toAnimatedToggle];
    if ShowModal = idOK then begin
      Application.ProcessMessages;
      WaitCursorInterface := WaitCursor;
      // Generate code
      Result := Format(Header, [FModuleName]);
      ExplorerTree.InitRecursive(ExplorerTree.RootNode, MaxInt, False);
      Node := ExplorerTree.RootNode^.FirstChild^.FirstChild;
      while Assigned(Node) do begin
        Symbol := SymbolFromNode(Node);
        SName := Symbol.name;
        if (Node.CheckState in [csCheckedNormal, csCheckedPressed,
          csMixedNormal, csMixedPressed]) then
        begin
          if Node.ChildCount > 0 then begin
            // Class Symbol
            Result := Result + Format(ClassHeader, [SName]);
            MethodNode := Node.FirstChild;
            while Assigned(MethodNode) do begin
              if (MethodNode.CheckState in [csCheckedNormal, csCheckedPressed]) then begin
                MSymbol := SymbolFromNode(MethodNode);
                MName := MSymbol.name;
                Result := Result + Format(MethodHeader, [MName]);
              end;
              MethodNode := MethodNode.NextSibling;
            end;
          end
          else
          begin
            if FunctionTests = '' then
              FunctionTests := Format(ClassHeader, ['GlobalFunctions']);
            FunctionTests := FunctionTests + Format(MethodHeader, [SName]);
          end;
        end;
        Node := Node.NextSibling;
      end;
      if FunctionTests <> '' then
        Result := Result + FunctionTests;
      Result := Result + Footer;
    end;
    Release;
    Symbols := [];
  end;
end;

procedure TUnitTestWizard.ExplorerTreeGetHint(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex;
  var LineBreakStyle: TVTTooltipLineBreakStyle; var HintText: string);
begin
  case ExplorerTree.GetNodeLevel(Node) of
    0:  HintText := Format(_('Python Module "%s"'), [FModuleName]);
    1, 2:
      begin
        var Symbol := SymbolFromNode(Node);
        HintText := SymbolHint(Symbol);
      end;
  else
    raise Exception.Create('TUnitTestWizard.ExplorerTreeGetHint');
  end;
end;

procedure TUnitTestWizard.ExplorerTreeGetImageIndex(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
  var Ghosted: Boolean; var ImageIndex: TImageIndex);
begin
  if Kind in [ikNormal, ikSelected] then
    case ExplorerTree.GetNodeLevel(Node) of
      0: ImageIndex := Integer(TCodeImages.Python);
      1: if Node.ChildCount = 0 then
           ImageIndex := Integer(TCodeImages.Func)
         else
           ImageIndex := Integer(TCodeImages.Klass);
      2: ImageIndex := Integer(TCodeImages.Method);
      else
        raise Exception.Create('TUnitTestWizard.ExplorerTreeGetImageIndex');
    end;
end;

procedure TUnitTestWizard.ExplorerTreeGetText(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
  var CellText: string);
begin
  if TextType = ttNormal then
    case ExplorerTree.GetNodeLevel(Node) of
      0:  CellText := FModuleName;
      1:  begin
            var Symbol := SymbolFromNode(Node);
            if Node.ChildCount = 0 then
              CellText := SymbolSignature(Symbol)
            else
              CellText := Symbol.name;
          end;
      2:  begin
            var Symbol := SymbolFromNode(Node);
            CellText := SymbolSignature(Symbol);
          end;
      else
        raise Exception.Create('TUnitTestWizard.ExplorerTreeGetText');
    end;
end;

procedure TUnitTestWizard.ExplorerTreeInitChildren(Sender: TBaseVirtualTree;
  Node: PVirtualNode; var ChildCount: Cardinal);
begin
  case ExplorerTree.GetNodeLevel(Node) of
    0: ChildCount := Length(Symbols);
    1:
       begin
         ChildCount := 0;
         var Value := SymbolFromNode(Node);
         ChildCount := Length(Value.children);
       end;
    2: ChildCount := 0;
    else
      raise Exception.Create('TUnitTestWizard.ExplorerTreeInitChildren');
  end;
end;

procedure TUnitTestWizard.ExplorerTreeInitNode(Sender: TBaseVirtualTree;
  ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
begin
  Node.CheckState := csCheckedNormal;
  Node.CheckType := ctCheckBox;
  case ExplorerTree.GetNodeLevel(Node) of
    0:
       begin
         InitialStates := [ivsHasChildren, ivsExpanded];
         Node.CheckType := ctTriStateCheckBox;
       end;
    1:
       begin
         var Child := Symbols[Node.Index];
         if Child.kind = TLSPSymbolKind.symClass then
         begin
           InitialStates := [ivsHasChildren, ivsExpanded];
           Node.CheckType := ctTriStateCheckBox;
         end;
       end;
    2:
      begin
        var Method := SymbolFromNode(Node);
        if Method.name = '__init__' then
          Node.CheckState := csUncheckedNormal;
      end;
    else
      raise Exception.Create('TUnitTestWizard.ExplorerTreeInitNode');
  end;
end;

procedure TUnitTestWizard.FormCreate(Sender: TObject);
begin
  inherited;
  ExplorerTree.NodeDataSize := 0;
end;

procedure TUnitTestWizard.HelpButtonClick(Sender: TObject);
begin
  if HelpContext <> 0 then
    Application.HelpContext(HelpContext);
end;

procedure TUnitTestWizard.mnDeselectAllClick(Sender: TObject);
var
  Node: PVirtualNode;
begin
   Node := ExplorerTree.RootNode^.FirstChild;
   while Assigned(Node) do begin
     ExplorerTree.CheckState[Node] := csUncheckedNormal;
     Node := Node.NextSibling;
   end;
end;

procedure TUnitTestWizard.mnSelectAllClick(Sender: TObject);
var
  Node: PVirtualNode;
begin
   Node := ExplorerTree.RootNode^.FirstChild;
   while Assigned(Node) do begin
     ExplorerTree.CheckState[Node] := csCheckedNormal;
     Node := Node.NextSibling;
   end;
end;

function TUnitTestWizard.SymbolFromNode(Node: PVirtualNode): TLSPDocumentSymbol;
begin
  var Level := ExplorerTree.GetNodeLevel(Node);
  if Level = 1 then
    Result := Symbols[Node.Index]
  else if Level = 2 then
    Result := Symbols[Node.Parent.Index].children[Node.Index]
  else
    Result := Default(TLSPDocumentSymbol);
end;

function TUnitTestWizard.SymbolHint(Symbol: TLSPDocumentSymbol): string;
begin
  var BC := BufferCoordFromLspPosition(Symbol.selectionRange.start);
  Result := TPyLspClient.SimpleHintAtCoordinates(FModuleFileName, BC);
  Result := StringReplace(Result, '<br>', SLineBreak, [rfReplaceAll]);
end;

function TUnitTestWizard.SymbolSignature(Symbol: TLSPDocumentSymbol): string;
begin
  var Line := Symbol.selectionRange.start.line;
  Result := Trim(GetNthSourceLine(FModuleFileName, Line + 1));
  var Index := Result.LastIndexOf(':');
  if Index >= 0 then
    Delete(Result, Index + 1, MaxInt);
  if Result.StartsWith('def') then
    Delete(Result, 1, 3);
  Result := Result.TrimLeft;
end;

end.
